php security patterns

安装量: 96
排名: #8518

安装

npx skills add https://github.com/thebushidocollective/han --skill PHP

Security is paramount in PHP applications as they often handle sensitive user data, authentication, and financial transactions. PHP's flexibility and dynamic nature create opportunities for vulnerabilities if security best practices aren't followed.

Common PHP security vulnerabilities include SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), insecure password storage, session hijacking, and file inclusion attacks. Each can lead to data breaches, unauthorized access, or complete system compromise.

This skill covers input validation and sanitization, SQL injection prevention, XSS protection, CSRF defense, secure password handling, session security, file upload security, and defense-in-depth strategies.

Input Validation and Sanitization

Input validation ensures data meets expected formats before processing, while sanitization removes or encodes potentially dangerous content.

<?php
declare(strict_types=1);

// Email validation
function validateEmail(string $email): bool {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// URL validation
function validateUrl(string $url): bool {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
}

// Integer validation
function validateInt(mixed $value, int $min = PHP_INT_MIN,
                     int $max = PHP_INT_MAX): ?int {
    $int = filter_var($value, FILTER_VALIDATE_INT, [
        'options' => [
            'min_range' => $min,
            'max_range' => $max,
        ],
    ]);

    return $int !== false ? $int : null;
}

// String sanitization
function sanitizeString(string $input): string {
    // Remove null bytes and control characters
    $sanitized = str_replace("\0", '', $input);
    $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized);
    return trim($sanitized);
}

// HTML sanitization (output encoding)
function sanitizeHtml(string $input): string {
    return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// Example usage
class UserRegistration {
    private array $errors = [];

    public function validate(array $data): bool {
        // Validate email
        if (!isset($data['email']) || !validateEmail($data['email'])) {
            $this->errors[] = 'Invalid email address';
        }

        // Validate age
        $age = validateInt($data['age'] ?? null, 13, 120);
        if ($age === null) {
            $this->errors[] = 'Age must be between 13 and 120';
        }

        // Validate username (alphanumeric, 3-20 chars)
        $username = sanitizeString($data['username'] ?? '');
        if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
            $this->errors[] = 'Username must be 3-20 alphanumeric characters';
        }

        // Validate password strength
        if (!$this->validatePassword($data['password'] ?? '')) {
            $this->errors[] = 'Password must be at least 8 characters ' .
                'with mixed case and numbers';
        }

        return empty($this->errors);
    }

    private function validatePassword(string $password): bool {
        return strlen($password) >= 8
            && preg_match('/[A-Z]/', $password)
            && preg_match('/[a-z]/', $password)
            && preg_match('/[0-9]/', $password);
    }

    public function getErrors(): array {
        return $this->errors;
    }
}

// Whitelist validation for enums
function validateStatus(string $status): ?string {
    $allowed = ['pending', 'approved', 'rejected'];
    return in_array($status, $allowed, true) ? $status : null;
}

// Complex data validation
function validateUserData(array $data): array {
    $validated = [];

    // Required fields
    $validated['email'] = validateEmail($data['email'] ?? '')
        ? $data['email']
        : throw new InvalidArgumentException('Invalid email');

    // Optional fields with defaults
    $validated['age'] = validateInt($data['age'] ?? 0, 0, 150) ?? 18;
    $validated['name'] = sanitizeString($data['name'] ?? '');

    // Nested validation
    if (isset($data['address'])) {
        $validated['address'] = [
            'street' => sanitizeString($data['address']['street'] ?? ''),
            'city' => sanitizeString($data['address']['city'] ?? ''),
            'zip' => preg_match('/^\d{5}$/', $data['address']['zip'] ?? '')
                ? $data['address']['zip']
                : null,
        ];
    }

    return $validated;
}

Always validate input at the application boundary and sanitize before output to prevent injection attacks.

SQL Injection Prevention

SQL injection occurs when user input is directly interpolated into SQL queries, allowing attackers to manipulate queries.

<?php
declare(strict_types=1);

// UNSAFE: Direct string concatenation
function findUserUnsafe(PDO $pdo, string $email): ?array {
    // NEVER DO THIS - vulnerable to SQL injection
    $sql = "SELECT * FROM users WHERE email = '$email'";
    $result = $pdo->query($sql);
    return $result ? $result->fetch(PDO::FETCH_ASSOC) : null;
}

// SAFE: Prepared statements with PDO
function findUserSafe(PDO $pdo, string $email): ?array {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
    $stmt->execute(['email' => $email]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result !== false ? $result : null;
}

// Safe: Positional parameters
function findUserById(PDO $pdo, int $id): ?array {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result !== false ? $result : null;
}

// Safe: Multiple parameters
function findUsersByStatus(PDO $pdo, string $status, int $limit): array {
    $stmt = $pdo->prepare(
        'SELECT * FROM users WHERE status = :status LIMIT :limit'
    );
    $stmt->bindValue(':status', $status, PDO::PARAM_STR);
    $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Safe: IN clause with placeholders
function findUsersByIds(PDO $pdo, array $ids): array {
    // Validate all IDs are integers
    $ids = array_filter($ids, 'is_int');
    if (empty($ids)) {
        return [];
    }

    // Create placeholders: ?,?,?
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $sql = "SELECT * FROM users WHERE id IN ($placeholders)";

    $stmt = $pdo->prepare($sql);
    $stmt->execute($ids);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Repository pattern with prepared statements
class UserRepository {
    public function __construct(
        private PDO $pdo
    ) {}

    public function find(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result !== false ? $result : null;
    }

    public function create(array $data): int {
        $stmt = $this->pdo->prepare(
            'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)'
        );
        $stmt->execute([
            $data['name'],
            $data['email'],
            $data['password_hash'],
        ]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, array $data): bool {
        $stmt = $this->pdo->prepare(
            'UPDATE users SET name = ?, email = ? WHERE id = ?'
        );
        return $stmt->execute([
            $data['name'],
            $data['email'],
            $id,
        ]);
    }

    public function delete(int $id): bool {
        $stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
        return $stmt->execute([$id]);
    }

    public function search(string $query, int $limit = 10): array {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?'
        );
        $pattern = "%$query%";
        $stmt->bindValue(1, $pattern, PDO::PARAM_STR);
        $stmt->bindValue(2, $pattern, PDO::PARAM_STR);
        $stmt->bindValue(3, $limit, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// Query builder with parameterization
class QueryBuilder {
    private array $where = [];
    private array $params = [];

    public function where(string $column, mixed $value): self {
        $placeholder = ':param' . count($this->params);
        $this->where[] = "$column = $placeholder";
        $this->params[$placeholder] = $value;
        return $this;
    }

    public function execute(PDO $pdo): array {
        $sql = 'SELECT * FROM users';
        if (!empty($this->where)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->where);
        }

        $stmt = $pdo->prepare($sql);
        $stmt->execute($this->params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

Always use prepared statements with parameter binding - never concatenate user input into SQL queries.

Cross-Site Scripting (XSS) Prevention

XSS attacks inject malicious scripts into web pages viewed by other users. Proper output encoding prevents script execution.

data[$key] = $value; } public function render(string $template): string { // Extract and escape all variables extract(array_map(function($value) { if (is_string($value)) { return escapeHtml($value); } return $value; }, $this->data)); ob_start(); include $template; return ob_get_clean(); } public function raw(string $key): string { // For trusted HTML - use sparingly return $this->data[$key] ?? ''; } } // Example template usage class CommentDisplay { public function renderComment(array $comment): string { $author = escapeHtml($comment['author']); $text = escapeHtml($comment['text']); $timestamp = escapeHtml($comment['timestamp']); return <<<HTML
{$author}
{$text}
{$timestamp}